package com.wmada.toolkit.rpc;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author will.ma
 * @date 2020/11/2
 */
public class CmdExecutor {

    private final AtomicBoolean isRunning = new AtomicBoolean(false);

    private final String       appPath;
    private final List<String> args        = new ArrayList<>();
    private final List<String> defaultArgs = new ArrayList<>();

    private Process process = null;

    public CmdExecutor(String appPath) {
        this.appPath = appPath;

        this.defaultArgs.add(appPath);

        this.args.addAll(this.defaultArgs);
    }

    public CmdExecutor(String appPath, String... defaultArgs) {
        this.appPath = appPath;

        this.defaultArgs.add(appPath);
        this.defaultArgs.addAll(Arrays.asList(defaultArgs));

        this.args.addAll(this.defaultArgs);
    }

    public String getAppPath() {
        return appPath;
    }

    public List<String> getArgs() {
        return Collections.unmodifiableList(args);
    }

    public List<String> getDefaultArgs() {
        return Collections.unmodifiableList(defaultArgs);
    }

    public CmdExecutor reset() {
        this.args.clear();
        this.args.addAll(this.defaultArgs);
        return this;
    }

    public CmdExecutor withArgs(String... args) {
        this.reset();
        this.args.addAll(Arrays.asList(args));
        return this;
    }

    public CmdExecutor addArgs(String... args) {
        this.args.addAll(Arrays.asList(args));
        return this;
    }

    public static final class ProcessKiller extends Thread {
        private Process process = null;

        public ProcessKiller() {
        }

        public void setProcess(Process process) {
            this.process = process;
        }

        @Override
        public void run() {
            if (process != null) {
                process.destroyForcibly();
            }
        }
    }

    public static final class ExceptionHolder<T extends Exception> {

        private T exception = null;

        public ExceptionHolder() {
        }

        public T getException() {
            return exception;
        }

        public void setException(T exception) {
            this.exception = exception;
        }

        public boolean hasException() {
            return exception != null;
        }

        public void test() throws T {
            if (hasException()) {
                throw exception;
            }
        }
    }

    public void shutdown() {
        if (isRunning.get() && process != null) {
            process.destroyForcibly();
        }
    }

    public void execute(CmdParser parser) throws CmdExecuteException, CmdParseException {
        if (isRunning.compareAndSet(false, true)) {
            String[] cmd = args.toArray(new String[0]);

            Runtime       runtime       = Runtime.getRuntime();
            ProcessKiller processKiller = new ProcessKiller();
            runtime.addShutdownHook(processKiller);

            try {
                process = runtime.exec(cmd);
                processKiller.setProcess(process);

                evaluate(process, parser);
            } catch (IOException | InterruptedException e) {
                throw new CmdExecuteException(e);
            } finally {
                if (process != null) {
                    process.destroy();
                    process = null;
                }
                processKiller.setProcess(null);
                runtime.removeShutdownHook(processKiller);
            }
            isRunning.set(false);
        } else {
            throw new CmdExecuteException("Concurrent Execution");
        }
    }

    private void evaluate(Process process, CmdParser parser) throws CmdParseException, InterruptedException {
        ExceptionHolder<CmdParseException> infoParseHolder  = new ExceptionHolder<>();
        ExceptionHolder<CmdParseException> errorParseHolder = new ExceptionHolder<>();

        Thread infoThread = new Thread(() -> {
            try (InputStream inputStream = process.getInputStream();
                 InputStreamReader bufferedReader = new InputStreamReader(inputStream);
                 BufferedReader reader = new BufferedReader(bufferedReader)) {
                String line;
                while ((line = reader.readLine()) != null) {
                    parser.parse(line);
                }
            } catch (IOException e) {
                infoParseHolder.setException(new CmdParseException(e));
            } catch (CmdParseException e) {
                infoParseHolder.setException(e);
            }
        });

        Thread errorThread = new Thread(() -> {
            try (InputStream inputStream = process.getErrorStream();
                 InputStreamReader bufferedReader = new InputStreamReader(inputStream);
                 BufferedReader reader = new BufferedReader(bufferedReader)) {
                String line;
                while ((line = reader.readLine()) != null) {
                    parser.parseError(line);
                }
            } catch (IOException e) {
                errorParseHolder.setException(new CmdParseException(e));
            } catch (CmdParseException e) {
                errorParseHolder.setException(e);
            }
        });

        infoThread.start();
        errorThread.start();

        infoThread.join();
        errorThread.join();

        infoParseHolder.test();
        errorParseHolder.test();
    }

}
